dojo.require('dojo.date');
dojo.require('dojo.number');
dojo.require('dojox.timing._base');

var atcaAntenna = function(spec, my) {
	/**
	 * The object we return to our caller.
	 * @type {object}
	 */
	var that = {};
	
	// Set some sensible defaults in our spec object.
	spec = spec || {};
	
	/**
	 * The antenna to handle.
	 * @type {string}
	 */
	spec.antenna = spec.antenna || '';
	
	/**
	 * The MoniCA handler object.
	 * @type {object}
	 */
	spec.monica = spec.monica || null;
	
	/**
	 * The function to call when we are ready to update the DOM.
	 * @type {function}
	 */
	spec.callback = spec.callback || null;
	
	/**
	 * The telescope component we use to keep ourselves updated.
	 * @type {telescopeComponent}
	 */
	var ourComponent;

	/**
	 * The MoniCA points we will handle that are always shown.
	 * @type {array}
	 */
	var ourPoints = [
			{ point: 'servo.State',
				title: 'State' },
			{ point: 'servo.coords.AzSkyPos',
				title: 'Azimuth' },
			{	point: 'servo.coords.ElSkyPos',
				title: 'Elevation' },
			{	point: 'servo.AzError',
				title: 'Azimuth Error' },
			{	point: 'servo.ElError',
				title: 'Elevation Error' },
			{	point: 'misc.turret.feed',
				title: 'Receiver Position' },
		  { point: 'power.powerSource',
				title: 'Power Source'	}
	];

	/**
	 * The MoniCA points we will handle that aren't always shown.
	 * @type {array}
	 */
	var ourAuxPoints = [
	    { point: 'misc.obs.caobsAntState',
				title: 'caobs Antenna Status'	}
	];
	
	// Our methods follow.

	// Our private methods.
	/**
	 * Initialise.
	 */
	var initialise = function() {
		// Check for an appropriate antenna name.
		var isAtca = /^ca0\d$/.test(spec.antenna);
		
		if (!isAtca) {
			return;
		}
		
		// Check for a MoniCA handler.
		if (spec.monica === null) {
			return;
		}
		
		// Make a new telescope component object.
		ourComponent = telescopeComponent({
			monica: spec.monica
		});
		
		// Add the points we care about.
		var requestedPoints = [];
		// Add them to the telescope component.
		for (var i = 0; i < ourPoints.length; i++) {
			requestedPoints[i] = spec.antenna + '.' +
				ourPoints[i].point;
		}
		for (i = 0; i < ourAuxPoints.length; i++) {
			requestedPoints.push(spec.antenna + '.' +
				ourAuxPoints[i].point);
		}
		ourComponent.addPoints(requestedPoints);
		// Make the component check itself every second.
		ourComponent.setCheckConditions({
			time: 1,
			callback: componentChecked
		});
	};

	/**
	 * This function gets called whenever our telescope component checks itself.
	 * @param {object} component The reference to the telescopeComponent.
	 */
	var componentChecked = function(component) {
		// Simply call our DOM update callback function to tell them we have
		// received a check update.
		if (spec.callback !== null &&
				dojo.isFunction(spec.callback)) {
			spec.callback('checked', that);
		}
	};
	
	
	// Our public methods.
	/**
	 * Initialise our object, which is only required if the user had
	 * asked us to delay construction initialisation.
	 */
	that.initialise = function() {
		initialise();
	};
	
	/**
	 * Get the list of points we handle.
	 */
	that.getPoints = function() {
		return ourPoints;
	};

	/**
	 * Get the list of point references and return them.
	 */
	that.getInformation = function() {
		return ourComponent.getPointInformation();
	};

	/**
	 * Get only a specific point reference.
	 * @param {string} pointName The name of the point to find and return, or
	 *                           its title.
	 */
	that.pointReference = function(pointName) {
		for (var pr = 0; pr < ourPoints.length; pr++) {
			if (ourPoints[pr].point === pointName ||
					ourPoints[pr].title === pointName) {
				return ourComponent.getSinglePoint(spec.antenna + '.' +
																					 ourPoints[pr].point);
			}
		}
		for (pr = 0; pr < ourAuxPoints.length; pr++) {
			if (ourAuxPoints[pr].point === pointName ||
					ourAuxPoints[pr].title === pointName) {
				return ourComponent.getSinglePoint(spec.antenna + '.' +
																					 ourAuxPoints[pr].point);
			}
		}
		
		return null;
	};
	
	/**
	 * Calculate the slew time for an ATCA antenna.
	 * @param {Number} azErr The distance in azimuth (degrees).
	 * @param {Number} elErr The distance in elevation (degrees).
	 */
	that.slewTime = function(azErr, elErr) {
		var timeSlew = 0;
		var deg2rad = Math.PI/180;
		var azDist = azErr * deg2rad;
		var elDist = elErr * deg2rad;
		
		var azSlewVel = 38 / 60 * deg2rad;
		var elSlewVel = 19 / 60 * deg2rad;
		var accel = 800 / 3600 * deg2rad;
		var azCrit = 0.5 * azSlewVel * azSlewVel / accel;
		var elCrit = 0.5 * elSlewVel * elSlewVel / accel;
		var azTimeCrit = 2 * azSlewVel / accel;
		var elTimeCrit = 2 * elSlewVel / accel;

		var azTime, elTime;
		if (Math.abs(azDist) <= azCrit) {
			azTime = 2 * Math.sqrt(Math.abs(azDist) / accel);
		} else {
			azTime = azTimeCrit + (Math.abs(azDist) - azCrit) / azSlewVel;
		}
		if (Math.abs(elDist) <= elCrit) {
			elTime = 2 * Math.sqrt(Math.abs(elDist) / accel);
		} else {
			elTime = elTimeCrit + (Math.abs(elDist) - elCrit) / elSlewVel;
		}

		timeSlew = (azTime > elTime) ? azTime : elTime;
		
		return timeSlew; // In seconds.
	};
	
	// Do our initialisation.
	if (!spec.delayInitialisation) {
		initialise();
	}
	
	return that;
};

var atcaStatus = function(spec, my) {
	/**
	 * The object we return to our caller.
	 * @type {object}
	 */
	var that = {};
	
	// Set some sensible defaults in our spec object.
	spec = spec || {};
	
	/**
	 * The MoniCA handler object.
	 * @type {object}
	 */
	spec.monica = spec.monica || null;
	
	/**
	 * The function to call when we are ready to update the DOM.
	 * @type {function}
	 */
	spec.callback = spec.callback || null;
	
	/**
	 * The telescope component we use to keep ourselves updated.
	 * @type {telescopeComponent}
	 */
	var ourComponent;

	/**
	 * The MoniCA points we will handle that are always shown.
	 * @type {array}
	 */
	var displayPoints = [
			{ point: 'site.misc.obs.source',
				title: 'Source',
			  type: 'observing' },
			{ point: 'site.misc.obs.target1',
				title: 'Coordinate1',
			  type: 'observing' },
			{ point: 'site.misc.obs.target2',
				title: 'Coordinate2',
			  type: 'observing' },
			{ point: 'site.misc.obs.coordSys',
				title: 'Epoch',
			  type: 'observing' },
			{ point: 'site.misc.obs.freq1',
				title: 'F1',
			  type: 'observing' },
			{ point: 'site.misc.obs.freq2',
				title: 'F2',
			  type: 'observing' },
			{ point: 'site.misc.obs.corrConfig',
				title: 'CorrConfig',
			  type: 'correlator' },
			{ point: 'site.misc.obs.rpfits_name',
				title: 'RPFITS',
			  type: 'correlator' },
			{ point: 'site.misc.obs.schedFile',
				title: 'Schedule',
			  type: 'correlator' },
			{ point: 'site.environment.weather.Temperature',
				title: 'Temp.',
			  type: 'weather' },
			{ point: 'site.environment.weather.Pressure',
				title: 'Pres.',
			  type: 'weather' },
			{ point: 'site.environment.weather.RelHumidity',
				title: 'Humid.',
			  type: 'weather' },
			{ point: 'site.environment.weather.WindSpeed',
				title: 'WindSpd',
			  type: 'weather' },
			{ point: 'site.environment.weather.WindDir',
				title: 'WindDir',
			  type: 'weather' },
			{ point: 'seemon.environment.seemon.RMSPathNoise',
				title: 'SeeingRMS',
			  type: 'weather' }
	];

	/**
	 * The MoniCA points we will handle that aren't always shown.
	 * @type {array}
	 */
	var monitorPoints = [
	    { point: 'site.environment.weather.WindMax' }
	];
	
	/**
	 * Our information conditions.
	 * @type {array}
	 */
	var infoConditions = [
	    {
				id: 'highWindInfo',
				conditions: [ 
					{
						pointNames: [ 'site.environment.weather.WindMax' ],
						action: 'valueAbove',
						values: [ 10 ]
					}
				],
				signal: 'info',
				ignorable: true,
				ignored: false,
				msgFn: function(eConditions) {
					return 'The maximum windspeed exceeds ' +
						eConditions[0].values[0] + ' ' + 
						eConditions[0].pointDetails[0].units + ': ' +
						eConditions[0].pointValues[0].value + ' ' +
						eConditions[0].pointDetails[0].units;
				}
			}
	];
	
	// Our methods follow.

	// Our private methods.
	/**
	 * Initialise.
	 */
	var initialise = function() {
		// Check for a MoniCA handler.
		if (spec.monica === null) {
			return;
		}
		
		// Make a new telescope component object.
		ourComponent = telescopeComponent({
			monica: spec.monica
		});
		
		// Add the points we care about.
		var requestedPoints = [];
		// Add them to the telescope component.
		for (var rp = 0; rp < displayPoints.length; rp++) {
			requestedPoints[rp] = displayPoints[rp].point;
		}
		for (rp = 0; rp < monitorPoints.length; rp++) {
			requestedPoints.push(monitorPoints[rp].point);
		}
		ourComponent.addPoints(requestedPoints);
		// Add our error conditions.
		ourComponent.addErrorConditions(infoConditions);
		// Make the component check itself every second.
		ourComponent.setCheckConditions({
			time: 1,
			callback: componentChecked,
			errorCallback: componentError
		});
	};

	/**
	 * This function gets called whenever our telescope component checks itself.
	 * @param {object} component The reference to the telescopeComponent.
	 */
	var componentChecked = function(component) {
		// Alter our coordinate titles based on what coordinate system we have.
		var coordSys =
			that.pointReference('site.misc.obs.coordSys').latestValue().value;
		if (coordSys === 'J2000.0') {
			searchPoint('site.misc.obs.target1').title = 'RA';
			searchPoint('site.misc.obs.target2').title = 'Dec';
		}
		// Call our DOM update callback function to tell them we have
		// received a check update.
		if (spec.callback !== null &&
				dojo.isFunction(spec.callback)) {
			spec.callback('checked', that);
		}
	};

	/**
	 * This function gets called whenever our telescope component finds an
	 * error during a check.
	 * @param {object} component The reference to the telescopeComponent.
	 */
	var componentError = function(component) {
		// Call our DOM update callback function to tell them we have received
		// an error update.
		if (spec.callback != null &&
				dojo.isFunction(spec.callback)) {
			spec.callback('error', that);
		}
	};
	
	/**
	 * Get the point object for a particular point name.
	 * @param {string} pointName The name of the point to search for.
	 */
	var searchPoint = function(pointName) {
		for (var dp = 0; dp < displayPoints.length; dp++) {
			if (displayPoints[dp].point === pointName) {
				return displayPoints[dp];
			}
		}
		
		return null;
	};
	
	// Our public methods.
	/**
	 * Initialise our object, which is only required if the user had
	 * asked us to delay construction initialisation.
	 */
	that.initialise = function() {
		initialise();
	};
	
	/**
	 * Get the list of points we handle.
	 */
	that.getDisplayPoints = function() {
		return displayPoints;
	};

	/**
	 * Get the list of point references and return them.
	 */
	that.getInformation = function() {
		return ourComponent.getPointInformation();
	};

	/**
	 * Get only a specific point reference.
	 * @param {string} pointName The name of the point to find and return, or
	 *                           its title.
	 */
	that.pointReference = function(pointName) {
		for (var pr = 0; pr < displayPoints.length; pr++) {
			if (displayPoints[pr].point === pointName ||
					displayPoints[pr].title === pointName) {
				return ourComponent.getSinglePoint(displayPoints[pr].point);
			}
		}

		return null;
	};

	/**
	 * Get the list of our errors that are triggering.
	 */
	that.allErrors = function() {
		return ourComponent.getErrorConditions();
	};
	
	// Do our initialisation.
	if (!spec.delayInitialisation) {
		initialise();
	}
	
	return that;
};


var webAssistanceInfoMessage = function(spec, my) {

	var that = {};
	
	// Set some sensible defaults in the spec object.
	spec = spec || {};
	
	/**
	 * The object that controls what we display.
	 * @type {object}
	 */
	spec.condition = spec.condition || {};
	
	/**
	 * Our parent DOM node.
	 * @type {domNode}
	 */
	spec.parentNode = spec.parentNode || '';

	// Our DOM node.
	var ourNode = {};
	
	// Our methods follow.

	// Our private methods.
	/**
	 * Initialise our display.
	 */
	var initialise = function() {
		// Do some checks first.
		if (typeof spec.condition.id === 'undefined' ||
				spec.parentNode === null) {
			return;
		}
		
		// Make our node and attach it to the DOM.
		ourNode.top = dojo.create('div', {
			id: 'node' + spec.condition.id,
			'class': 'infoAssistanceMessage'
		});
		
		dojo.place(ourNode.top, spec.parentNode);
		
		// Make a space for the ignore button.
		ourNode.ignoreSpace = dojo.create('div', {
			'class': 'infoAssistanceIgnore',
			innerHTML: '&nbsp;'
		});
		ourNode.top.appendChild(ourNode.ignoreSpace);

		// Can we put an ignore button on this?
		if (spec.condition.ignorable === true) {
			ourNode.ignoreButton = dojo.create('button', {
				type: 'button',
				innerHTML: 'Ignore'
			});
			dojo.place(ourNode.ignoreButton, 
								 ourNode.ignoreSpace, 'only');
			dojo.connect(ourNode.ignoreButton, 'onclick', ignoreHandler);
		}
		
		// Put our message in the node.
		ourNode.message = dojo.create('span', {
			'class': 'infoAssistanceMessageSpan'
		});
		ourNode.top.appendChild(ourNode.message);
		printMessage();
	};

	/**
	 * Ignore handler.
	 */
	var ignoreHandler = function() {
		if (spec.condition.ignored === false) {
			ignoreUs();
		} else {
			unignoreUs();
		}
	};
	
	/**
	 * Ignore this error.
	 */
	var ignoreUs = function() {
		spec.condition.ignored = true;
		dojo.attr(ourNode.ignoreButton, 'innerHTML', 'Unignore');
	};

	/**
	 * Unignore this error.
	 */
	var unignoreUs = function() {
		spec.condition.ignored = false;
		dojo.attr(ourNode.ignoreButton, 'innerHTML', 'Ignore');
	};
	
	/**
	 * Put the message in the right place.
	 */
	var printMessage = function() {
		dojo.attr(ourNode.message, 'innerHTML', spec.condition.message);
	};
	
	// Our public methods.
	/**
	 * Return our ID.
	 */
	that.getId = function() {
		return spec.condition.id;
	};
	
	/**
	 * Change the message we should display.
	 * @param {string} newMessage The new message to display.
	 */
	that.updateMessage = function(newMessage) {
		if (dojo.isString(newMessage) === true) {
			spec.condition.message = newMessage;
			printMessage();
		}
	};

	// Do our initialisation.
	initialise();
	
	return that;
};

var atcaWebAssistance = function() {
	// Set up a MoniCA object.
	var setupOptions = {
		updateInterval: 1000, // Update every second.
		serverName: 'localhost',
		webserverName: 'localhost',
		webserverPath: '/cgi-bin/monicainterface_json.pl'
	};
	var monica = monicaServer(setupOptions);

	// A list of info messages.
	var infoMessages = [];
	
	var startAssistance = function(defObj) {
		// We get called when MoniCA starts.
		
		// Add a few antenna handlers.
		var antHandlers = [];
		var nAnts = 6;
		for (var a = 1; a <= nAnts; a++) {
			antHandlers[a - 1] = atcaAntenna({
				antenna: 'ca0' + a,
				monica: monica,
				callback: antennaDisplay,
				delayInitialisation: true
			});
		}
		
		// Make a table to hold the antenna information.
		var antTable = dojo.create('table', {
			id: 'antennaInformationTable',
			'class': 'atcaAntennaTable'
		});
		dojo.byId('antennaArea').appendChild(antTable);
		
		// Get the information from one of the handlers.
		var pointDetails = antHandlers[0].getPoints();

		// Put the antenna name first.
		var antRow = dojo.create('tr');
		antTable.appendChild(antRow);
		for (a = 0; a <= nAnts; a++) {
			if (a === 0) {
				dojo.place('<th>Antenna</th>', antRow);
			} else {
				var tTitle = dojo.create('td', {
					'class': 'titleca0' + a,
					innerHTML: 'CA0' + a
				});
				antRow.appendChild(tTitle);
			}
		}
		
		for (var r = 0; r < pointDetails.length; r++) {
			antRow = dojo.create('tr');
			antTable.appendChild(antRow);
			for (a = 0; a <= nAnts; a++) {
				if (a === 0) {
					dojo.place('<th>' + pointDetails[r].title + '</th>', antRow);
				} else {
					var tIdFull = 'ca0' + a + '.' + pointDetails[r].point;
					var myId = antSafeId(tIdFull);
					var tCell = dojo.create('td', {
						id: myId
					});
					antRow.appendChild(tCell);
				}
			}
		}

		for (a = 0; a < nAnts; a++) {
			antHandlers[a].initialise();
		}

		// Add a status handler.
		var statusHandler = atcaStatus({
			monica: monica,
			callback: statusDisplay,
			delayInitialisation: true
		});
		
		var statusPoints = statusHandler.getDisplayPoints();
		
		for (var s = 0; s < statusPoints.length; s++) {
			var sSpan = dojo.create('span', {
				'class': 'statusItemSpan'
			});
			dojo.byId('statusArea').appendChild(sSpan);
			var sTable = dojo.create('table', {
				'class': statusPoints[s].type + 'StatusTable'
			});
			sSpan.appendChild(sTable);
			var sRow = dojo.create('tr');
			sTable.appendChild(sRow);
			var myId = statusSafeId(statusPoints[s].point);
			sRow.appendChild(dojo.create('th', {
				id: myId + 'Title'
			}));
			sRow.appendChild(dojo.create('td', {
				id: myId + 'Value'
			}));
		}
		
		var sClear = dojo.create('div', {
			'class': 'statusItemClear'
		});
		dojo.byId('statusArea').appendChild(sClear);

		statusHandler.initialise();
		
		// Start MoniCA updating.
		monica.startUpdating();
	};

	var statusDisplay = function(callType, callObj) {
		// What we do depends on how we were called.
		if (callType === 'checked') {
			// A normal update, and we get all the display points.
			var dPoints = callObj.getDisplayPoints();
			
			for (var p = 0; p < dPoints.length; p++) {
				var stId = statusSafeId(dPoints[p].point);
				var stRef = callObj.pointReference(dPoints[p].point).latestValue();
				var stDets =
					callObj.pointReference(dPoints[p].point).getPointDetails();
				dojo.attr(stId + 'Title', 'innerHTML', dPoints[p].title);
				var tVal = stRef.value;
				if (stDets.units !== '') {
					tVal += ' ' + stDets.units;
				}
				dojo.attr(stId + 'Value', 'innerHTML', tVal);
			}
		} else if (callType === 'error') {
			// Find all the conditions that have triggered this error.
			var errConds = callObj.allErrors();
			for (var ae = 0; ae < errConds.length; ae++) {
				// How do we display the error?
				if (errConds[ae].signal === 'info') {
					addInfoMessage(errConds[ae]);
				}
			}
		}
	};

	var addInfoMessage = function(eCond) {
		// Check for the same one already.
		var messageFound = false;
		for (var ii = 0; ii < infoMessages.length; ii++) {
			if (infoMessages[ii].getId() === eCond.id) {
				// Just change the message.
				infoMessages[ii].updateMessage(eCond.message);
				messageFound = true;
			}
		}
		if (messageFound === false) {
			// Add a new message.
			infoMessages.push(webAssistanceInfoMessage({
				condition: eCond,
				parentNode: 'infoArea'
			}));
		}
	};
	
	var antennaDisplay = function(callType, callObj) {
		// What we do depends on how we were called.
		if (callType === 'checked') {
			// A normal update, and we get all the points.
			var aPoints = callObj.getInformation();
			
			// Check for an antenna with a non-normal caobs status.
			var caobsStatus =
				callObj.pointReference('misc.obs.caobsAntState').latestValue().value;
			var additionalClasses = [];
			var availableClasses = ['cellOffline'];
			if (caobsStatus.toLowerCase() === 'off-line') {
				additionalClasses.push('cellOffline');
			}
			for (var p = 0; p < aPoints.length; p++) {
				// Check for some points we don't want to display.
				if (/caobsAntState$/.test(aPoints[p].name) === true) {
					continue;
				}
				var pSId = antSafeId(aPoints[p].name);
				var pVal = aPoints[p].reference.latestValue().value;
				var pValDeg = aPoints[p].reference.latestValue({
					valueAsDecimalDegrees: true
				}).value;
				var sVal;
				if (pVal == pValDeg) {
					sVal = pVal;
				} else {
					if (/Error/.test(aPoints[p].name) === true) {
						// Determine which range to use.
						var uVal, uAppend;
						if (Math.abs(pValDeg) > 1) {
							uVal = pValDeg;
							uAppend = '&deg';
						} else if (Math.abs(pValDeg) > 0.0167) {
							uVal = pValDeg * 60;
							uAppend = '\'';
						} else {
							uVal = pValDeg * 3600;
							uAppend = '"';
						}
						sVal = dojo.number.format(uVal, {
							places: 1
						}) + uAppend;
					} else {
						sVal = dojo.number.format(pValDeg, {
							places: 3
						}) + '&deg';
					}
				}
				if (dojo.byId(pSId)) {
					dojo.attr(pSId, 'innerHTML', sVal);
				}
				
				// Check for some specific situations.
				if (/State$/.test(aPoints[p].name) === true &&
						pVal === 'SLEWING') {
					var errAz =
						callObj.pointReference('servo.AzError').latestValue({
							valueAsDecimalDegrees: true
						}).value;
					var errEl =
						callObj.pointReference('servo.ElError').latestValue({
							valueAsDecimalDegrees: true
						}).value;
					var sTime = callObj.slewTime(errAz, errEl);
					dojo.attr(pSId, 'innerHTML', pVal + ' ' +
										dojo.number.round(sTime) + 's');
				}
				
				// Apply any additional classes we have.
				if (dojo.byId(pSId)) {
					for (var c = 0; c < availableClasses.length; c++) {
						var removeClass = true;
						for (var ac = 0; ac < additionalClasses.length; ac++) {
							if (availableClasses[c] === additionalClasses[ac]) {
								removeClass = false;
							}
						}
						if (removeClass === true) {
							dojo.removeClass(pSId, availableClasses[c]);
						} else {
							dojo.addClass(pSId, availableClasses[c]);
						}
					}
				}
			}
		}
	};
	
	var safifyPointName = function(pointName) {
		var stage1 = pointName.replace(/\./g, '');
		var stage2 = pointName.replace(/\+/g, 'plus');
		
		return stage2;
	};
	
	var antSafeId = function(idFull) {
		var tId = 'ant' + safifyPointName(idFull);
		
		return tId;
	};

	var statusSafeId = function(idFull) {
		var tId = 'status' + safifyPointName(idFull);
		
		return tId;
	};
	
	// And connect now.
	monica.connect().then(startAssistance);
	
	// Start displaying the time in the time area.
	var atcaTime = astroTime();
	dojo.place('<span>UTC: </span><span id="utcTime"></span>' +
						 '<span>Local: </span><span id="localTime"></span>' +
						 '<span>LMST: </span><span id="lmstTime"></span>',
		dojo.byId('timeArea'));
	// Update every half a second.
	var t = new dojox.timing.Timer(500);
	t.onTick = function() {
		var atcaUTC = atcaTime.now().timeString();
		dojo.attr('utcTime', 'innerHTML', atcaUTC);
		var atcaLocal = atcaTime.localtimeString();
		dojo.attr('localTime', 'innerHTML', atcaLocal);
		var atcaLMST = atcaTime.lmstString();
		dojo.attr('lmstTime', 'innerHTML', atcaLMST);
	};
	t.start();
	
};


dojo.addOnLoad(atcaWebAssistance);
